JDK-8299339 : HashMap merge and compute methods can cause odd resizing pathologies

贴一下去年发现的 Bug,嘿嘿。

导致 Bug 的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
Map<Integer, Integer> map = new HashMap<>(2);

map.merge(1, 1, Integer::sum);
map.merge(2, 1, Integer::sum);

map.forEach((k, v) -> {
map.merge(k, -1, Integer::sum);
System.out.println(k);
});
}
}

Java Bug DataBase 链接,里面比较详细的讨论了发生的问题,由于当时急着发出去,我的评论有点乱,而且是中文翻译为英文的,有点拉。


今天重新回顾一下 Bug,顺便简单解释一下。上述代码的输出为 2,期望输出是 2 和 1(顺序不重要)。问题在于,在两次 merge 之后,map 包含的元素数量 2 已经超过扩容阈值 1,下一次扩容发生在迭代中,导致不正确的输出。具体来说,在 resize 方法中,有 oldTab[j] = null; 操作,即转移元素到新数组时,会将旧数组的所有元素置为 null,从而旧数组的迭代器扫描不到剩余的元素。总的来说,即使在遍历的过程中,没有发生结构性的修改,在特定情况下使用 merge 方法修改 HashMap 依然会导致问题。特定情况是指,在遍历之前 HashMap 的包含的元素数量超过扩容阈值,然后在遍历的过程中使用 merge 方法。

ORACLE 的内部人员首先说明,由于调用 Map 上的方法可能产生破坏迭代的副作用,所以不建议在迭代的过程中调用 Map 上的方法,特别是具有副作用的方法,建议重写代码避免这种情况。然后提到 merge 方法确实有问题,即元素数量超过阈值也没有立即扩容,它和 put 方法的实现不同。评论中还列出其他具有类似副作用的方法,以及其他产生副作用的情况,详细看链接。

JDK-8299339 : HashMap merge and compute methods can cause odd resizing pathologies

https://ligh0x74.github.io/2023/10/09/JDK-8299339 HashMap merge and compute methods can cause odd resizing pathologies/

作者

Ligh0x74

发布于

2023-10-09

更新于

2025-01-18

许可协议

评论